// MailNoter - A capture tool for emails

// Copyright (C) 2009 - MailNoter

// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.

// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.

// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software Foundation,
// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
//
#include "StdAfx.h"
#include "MainWindow.h"
#include "registry.h"
#include "Utils.h"
#include "UnicodeUtils.h"
#include "OptionsDlg.h"
#include "AboutDlg.h"
#include "MailMsg.h"
#include "Styles.h"

#include <cctype>
#include <regex>

#define GET_X_LPARAM(lp) ((int)(short)LOWORD(lp))
#define GET_Y_LPARAM(lp) ((int)(short)HIWORD(lp))

#define TRAY_WM_MESSAGE		WM_APP+1

#define TIMER_CLIPWAIT 100

// Get clipboard id for HTML format...
static int CF_HTMLFORMAT = RegisterClipboardFormat(_T("HTML Format"));

static UINT WM_TASKBARCREATED = RegisterWindowMessage(_T("TaskbarCreated"));
CMainWindow::PFNCHANGEWINDOWMESSAGEFILTER CMainWindow::m_pChangeWindowMessageFilter = NULL;


#define PACKVERSION(major,minor) MAKELONG(minor,major)
DWORD CMainWindow::GetDllVersion(LPCTSTR lpszDllName)
{
	HINSTANCE hinstDll;
	DWORD dwVersion = 0;

	hinstDll = LoadLibrary(lpszDllName);

	if (hinstDll)
	{
		DLLGETVERSIONPROC pDllGetVersion;
		pDllGetVersion = (DLLGETVERSIONPROC)GetProcAddress(hinstDll, 
			"DllGetVersion");

		if (pDllGetVersion)
		{
			DLLVERSIONINFO dvi;
			HRESULT hr;

			ZeroMemory(&dvi, sizeof(dvi));
			dvi.cbSize = sizeof(dvi);

			hr = (*pDllGetVersion)(&dvi);

			if (SUCCEEDED(hr))
			{
				dwVersion = PACKVERSION(dvi.dwMajorVersion, dvi.dwMinorVersion);
			}
		}

		FreeLibrary(hinstDll);
	}
	return dwVersion;
}

bool CMainWindow::RegisterAndCreateWindow()
{
	WNDCLASSEX wcx; 

	// Fill in the window class structure with default parameters 
	wcx.cbSize = sizeof(WNDCLASSEX);
	wcx.style = CS_HREDRAW | CS_VREDRAW;
	wcx.lpfnWndProc = CWindow::stWinMsgHandler;
	wcx.cbClsExtra = 0;
	wcx.cbWndExtra = 0;
	wcx.hInstance = hResource;
	wcx.hCursor = NULL;
	wcx.lpszClassName = ResString(hResource, IDS_APP_TITLE);
	wcx.hIcon = LoadIcon(hResource, MAKEINTRESOURCE(IDI_MAILNOTER));
	wcx.hbrBackground = NULL;
	wcx.lpszMenuName = NULL;
	wcx.hIconSm	= LoadIcon(wcx.hInstance, MAKEINTRESOURCE(IDI_MAILNOTER));
	if (RegisterWindow(&wcx))
	{
		if (CreateEx(NULL, WS_POPUP, NULL))
		{
			// On Vista, the message TasbarCreated may be blocked by the message filter.
			// We try to change the filter here to get this message through. If even that
			// fails, then we can't do much about it and the task bar icon won't show up again.
			HMODULE hLib = LoadLibrary(_T("user32.dll"));
			if (hLib)
			{
				m_pChangeWindowMessageFilter = (CMainWindow::PFNCHANGEWINDOWMESSAGEFILTER)GetProcAddress(hLib, "ChangeWindowMessageFilter");
				if (m_pChangeWindowMessageFilter)
				{
					(*m_pChangeWindowMessageFilter)(WM_TASKBARCREATED, MSGFLT_ADD);
				}
			}

			ShowTrayIcon();
			return true;
		}
	}
	return false;
}

void CMainWindow::ShowTrayIcon()
{
	// since our main window is hidden most of the time
	// we have to add an auxiliary window to the system tray
	ZeroMemory(&niData,sizeof(NOTIFYICONDATA));

	ULONGLONG ullVersion = GetDllVersion(_T("Shell32.dll"));
	if (ullVersion >= MAKEDLLVERULL(6,0,0,0))
		niData.cbSize = sizeof(NOTIFYICONDATA);
	else if(ullVersion >= MAKEDLLVERULL(5,0,0,0))
		niData.cbSize = NOTIFYICONDATA_V2_SIZE;
	else niData.cbSize = NOTIFYICONDATA_V1_SIZE;

	niData.uID = IDI_MAILNOTER;
	niData.uFlags = NIF_ICON|NIF_MESSAGE|NIF_TIP|NIF_INFO;

	niData.hIcon = (HICON)LoadImage(hResource, MAKEINTRESOURCE(IDI_MAILNOTER),
		IMAGE_ICON, GetSystemMetrics(SM_CXSMICON), GetSystemMetrics(SM_CYSMICON), LR_DEFAULTCOLOR);
	niData.hWnd = *this;
	niData.uCallbackMessage = TRAY_WM_MESSAGE;

	Shell_NotifyIcon(NIM_DELETE,&niData);
	Shell_NotifyIcon(NIM_ADD,&niData);
	DestroyIcon(niData.hIcon);
}

LRESULT CMainWindow::HandleCustomMessages(HWND /*hwnd*/, UINT uMsg, WPARAM /*wParam*/, LPARAM /*lParam*/)
{
	if (uMsg == WM_TASKBARCREATED)
	{
		ShowTrayIcon();
	}
	return 0L;
}

LRESULT CALLBACK CMainWindow::WinMsgHandler(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
	switch (uMsg)
	{
	case WM_CREATE:
		{
			m_hwnd = hwnd;
			RegisterHotKeys();
		}
		break;
	case WM_COMMAND:
		return DoCommand(LOWORD(wParam));
		break;
	case WM_HOTKEY:
		{
			WORD key = MAKEWORD(HIWORD(lParam), LOWORD(lParam));
			key = CUtils::HotKey2HotKeyControl(key);
			CRegStdWORD regCaptureTextHK(_T("Software\\MailNoter\\captureTextHK"), DEFAULT_CAPTURE_HOTKEY);
			if (key == (WORD)(DWORD)regCaptureTextHK)
			{
				CaptureText();
			}
		}
		break;
	case TRAY_WM_MESSAGE:
		{
			switch(lParam)
			{
			case WM_LBUTTONDBLCLK:
				//StartPresentationMode();
				break;
			case WM_RBUTTONUP:
			case WM_CONTEXTMENU:
				{
					POINT pt;
					GetCursorPos(&pt);
					HMENU hMenu = LoadMenu(hResource, MAKEINTRESOURCE(IDC_MAILNOTER));
					HMENU hPopMenu = GetSubMenu(hMenu, 0);
					SetForegroundWindow(*this);
					TrackPopupMenu(hPopMenu, TPM_LEFTALIGN | TPM_RIGHTBUTTON, pt.x, pt.y, 0, *this, NULL); 
					DestroyMenu(hMenu);
				}
				break;
			}
		}
		break;
	case WM_DESTROY:
		Shell_NotifyIcon(NIM_DELETE,&niData);
		PostQuitMessage(0);
		break;
	case WM_CHANGECBCHAIN: 
		// If the next window is closing, repair the chain. 
		if ((HWND) wParam == hwndNextViewer) 
			hwndNextViewer = (HWND) lParam; 
		// Otherwise, pass the message to the next link. 
		else if (hwndNextViewer != NULL) 
			SendMessage(hwndNextViewer, uMsg, wParam, lParam); 
		break;
	case WM_DRAWCLIPBOARD:  // clipboard contents changed. 
		// Pass the message to the next window in clipboard 
		// viewer chain. 
		SendMessage(hwndNextViewer, uMsg, wParam, lParam);
		if (bCaptureClipboard)
		{
			KillTimer(*this, TIMER_CLIPWAIT);

			if (OpenClipboard(*this))
			{
				wstring content;
				vector<wstring> attachments;
				wstring title;
				if (IsClipboardFormatAvailable(CF_HTMLFORMAT))
				{
					HGLOBAL   hglb;
					LPSTR    lptstr;
					hglb = GetClipboardData(CF_HTMLFORMAT);
					if (hglb != NULL)
					{
						lptstr = (LPSTR)GlobalLock(hglb);
						if (lptstr != NULL)
						{
							wstring sourceURL;

							// find the line "SourceURL:xxxx"
							const char * pSrcUrl = CUtils::stristr(lptstr, "SourceURL:");
							if (pSrcUrl)
							{
								const char * pStart = strchr(pSrcUrl, ':')+1;
								const char * pEnd = strchr(pSrcUrl, '\n')-1;
								sourceURL = CUnicodeUtils::StdGetUnicode(string(pStart, pEnd-pStart));
							}

							// find the html body
							pSrcUrl = CUtils::stristr(lptstr, "<html>");
							if (pSrcUrl)
							{
								string source = string(pSrcUrl);
								source.erase( remove(source.begin(),source.end(),'\r') , source.end() );
								// now remove all newline chars:
								// MAPI was designed before html mails were around, so email clients have to
								// interpret mail body text passed to them via MAPI as *text* (not html).
								// While most clients detect html and show the mails correctly, they still
								// replace newlines with <br> tags to avoid garbling up plain text
								// but only remove the newlines if they're NOT inside a <pre> tag.
								string newsource;
								string icaseSource = source;
								transform(icaseSource.begin(), icaseSource.end(), icaseSource.begin(), tolower);
								size_t off = 0;
								size_t foundIndex = string::npos;
								while ((foundIndex = icaseSource.find("<pre", off))!=string::npos)
								{
									string noPreText = source.substr(off, foundIndex-off);
									noPreText.erase( remove(noPreText.begin(),noPreText.end(),'\n') , noPreText.end() );
									newsource += noPreText;
									// now find the closing </pre> tag
									off = foundIndex;
									size_t closeIndex = icaseSource.find("</pre", foundIndex);
									if (closeIndex != string::npos)
									{
										newsource += source.substr(foundIndex, closeIndex-foundIndex);
										off = closeIndex;
									}
								}
								string lastPart = source.substr(off);
								lastPart.erase( remove(lastPart.begin(),lastPart.end(),'\n') , lastPart.end() );
								newsource += lastPart;

								const tr1::regex pattern1("<(h1|h2|h3)[^>]*>(.*?)</(h1|h2|h3)>", tr1::regex_constants::icase | tr1::regex_constants::ECMAScript);

								// object that will contain the sequence of sub-matches
								tr1::match_results<string::const_iterator> result;

								// match the title with the regular expression
								if (tr1::regex_search(newsource, result, pattern1))
								{
									title = CUnicodeUtils::StdGetUnicode(result[2]);
								}

								if (!title.empty())
								{
									// remove garbage in the title
									tr1::wregex rx(_T("(<[^>]*>)"));
									wstring replacement = _T("");
									title = tr1::regex_replace(title, rx, replacement);
									// now remove double spaces
									tr1::wregex rx2(_T("( +)"));
									replacement = _T(" ");
									title = tr1::regex_replace(title, rx2, replacement);
								}


								content = CUnicodeUtils::StdGetUnicode(newsource);

								// append the source URL at the bottom of the content
								size_t lastpos = content.find_last_of('/', content.find_last_of('/')-1);
								wstring closingTags = content.substr(lastpos - 1);
								content = content.substr(0, lastpos - 1);

								CRegStdString regAppendix(_T("Software\\MailNoter\\appendix"), DEFAULT_APPENDIX);
								wstring appendix = regAppendix;
								// replace "%sourceURL" with the source URL
								wstring tag = _T("%sourceURL");
								wstring::iterator it_begin = search(appendix.begin(), appendix.end(), tag.begin(), tag.end());
								if (it_begin != appendix.end())
								{
									wstring::iterator it_end= it_begin + tag.size();
									appendix.replace(it_begin, it_end, sourceURL);
								}
								appendix.erase( remove(appendix.begin(),appendix.end(),'\n') , appendix.end() );
								appendix.erase( remove(appendix.begin(),appendix.end(),'\r') , appendix.end() );
								content += appendix;

								content += HTML_STYLES;
								content += closingTags;
							}

							GlobalUnlock(hglb);
						}
					}
				}
				else if (IsClipboardFormatAvailable(CF_UNICODETEXT))
				{
					HGLOBAL   hglb;
					hglb = GetClipboardData(CF_UNICODETEXT);
					if (hglb != NULL)
					{
						LPCTSTR lpstr = (LPCTSTR)GlobalLock(hglb);
						content = lpstr;
						GlobalUnlock(hglb); 
					}
				}
				else if (IsClipboardFormatAvailable(CF_HDROP))
				{
					HGLOBAL hglb = GetClipboardData(CF_HDROP);
					HDROP hDrop = (HDROP)GlobalLock(hglb);
					vector<wstring> imageUrls;
					if(hDrop != NULL)
					{
						TCHAR szFileName[MAX_PATH];
						UINT cFiles = DragQueryFile(hDrop, 0xFFFFFFFF, NULL, 0); 
						for(UINT i = 0; i < cFiles; ++i)
						{
							DragQueryFile(hDrop, i, szFileName, sizeof(szFileName)/sizeof(TCHAR));
							wstring filename = szFileName;
							if (CUtils::IsImage(filename))
							{
								imageUrls.push_back(CUtils::GetFileUrl(filename));
							}
							else
								attachments.push_back(filename);
						}
						GlobalUnlock(hDrop);
					}
					if (imageUrls.size())
					{
						content = _T("<html><body>");
						for (vector<wstring>::iterator it = imageUrls.begin(); it != imageUrls.end(); ++it)
						{
							content += _T("<p align=center><img src=\"");
							content += *it;
							content += _T("\"><p>");
						}
						content += _T("</body></html>");
					}
				}
				CloseClipboard();

				if (!content.empty())
				{
					if (title.empty())
					{
						// to get a subject for the mail, find the window title of
						// the window where we got the content from
						HWND hWnd = GetForegroundWindow();
						if (hWnd)
						{
							TCHAR buf[MAX_PATH] = {0};
							::GetWindowText(hWnd, buf, 1024);
							title = buf;
							// most applications append use the format "doc title - appname"
							// so we only use the text up to the first '-' char
							size_t dashPos = title.find_last_of('-');
							if (dashPos > 0)
								title = title.substr(0, dashPos-1);
						}
					}

					CRegStdString regMailTo(_T("Software\\MailNoter\\mailto"));
					CRegStdString regMailFrom(_T("Software\\MailNoter\\mailfrom"));

					CMailMsg mailer;
					mailer.SetMessage(WideToMultibyte(content));
					mailer.SetTo(WideToMultibyte((wstring)regMailTo));
					mailer.SetFrom(WideToMultibyte((wstring)regMailFrom));
					mailer.SetSubject(WideToMultibyte(title));
					if (attachments.size())
					{
						for (vector<wstring>::iterator it = attachments.begin(); it != attachments.end(); ++it)
						{
							size_t slashpos = it->find_last_of('\\');
							wstring name = slashpos > 0 ? it->substr(slashpos+1) : *it;
							mailer.AddAttachment(WideToMultibyte(*it), WideToMultibyte(name));
						}
					}
					mailer.Send();
				}
				else if (attachments.size())
				{
					CRegStdString regMailTo(_T("Software\\MailNoter\\mailto"));
					CRegStdString regMailFrom(_T("Software\\MailNoter\\mailfrom"));

					CMailMsg mailer;
					mailer.SetTo(WideToMultibyte((wstring)regMailTo));
					mailer.SetFrom(WideToMultibyte((wstring)regMailFrom));
					for (vector<wstring>::iterator it = attachments.begin(); it != attachments.end(); ++it)
					{
						size_t slashpos = it->find_last_of('\\');
						wstring name = slashpos > 0 ? it->substr(slashpos+1) : *it;
						mailer.AddAttachment(WideToMultibyte(*it), WideToMultibyte(name));
					}
					mailer.Send();
				}
			}
			ChangeClipboardChain(*this, hwndNextViewer); 
		}
		break; 
	case WM_TIMER:
		if (wParam == TIMER_CLIPWAIT)
		{
			ChangeClipboardChain(*this, hwndNextViewer); 
		}
		KillTimer(*this, TIMER_CLIPWAIT);
		break;
	default:
		return DefWindowProc(hwnd, uMsg, wParam, lParam);
	}

	return 0;
};

LRESULT CMainWindow::DoCommand(int id)
{
	switch (id) 
	{

	case IDM_EXIT:
		Shell_NotifyIcon(NIM_DELETE,&niData);
		::PostQuitMessage(0);
		return 0;
		break;
	case IDM_ABOUT:
		{
			CAboutDlg dlg(NULL);
			dlg.DoModal(hResource, IDD_ABOUTBOX, NULL);
		}
		break;
	case IDM_OPTIONS:
		{
			UnregisterHotKey(*this, CAPTURE_HOTKEY);
			COptionsDlg dlg(NULL);
			dlg.DoModal(hResource, IDD_OPTIONS, NULL);
			RegisterHotKeys();
		}
		break;
	default:
		break;
	};
	return 1;
}


void CMainWindow::RegisterHotKeys()
{
	CRegStdWORD regCaptureTextHK(_T("Software\\MailNoter\\captureTextHK"), DEFAULT_CAPTURE_HOTKEY);
	WORD captureTextHK = (WORD)(DWORD)regCaptureTextHK;
	captureTextHK = CUtils::HotKeyControl2HotKey(captureTextHK);

	if (!RegisterHotKey(*this, CAPTURE_HOTKEY, HIBYTE(captureTextHK), LOBYTE(captureTextHK)))
	{
		::MessageBox(*this, _T("Failed to register the hotkey!\n\nThis can happen if another application has already registered the same hotkey.\nPlease choose another hotkey."), _T("MailNoter"), MB_ICONERROR);
		PostMessage(*this, WM_COMMAND, IDM_OPTIONS, 0);
	}
}

bool CMainWindow::CaptureText()
{
	// when capturing, first register ourselves with the clipboard chain to
	// get notified every time the clipboard content changes.
	// Then send a "Ctrl-C" key combination to the foreground window


	// Since registering ourselves with the clipboard chain triggers a change
	// notification, we have to ignore that notification since we're
	// waiting for the one we cause ourselves
	bCaptureClipboard = false;
	HWND hwndNextViewer = SetClipboardViewer(*this);
	bCaptureClipboard = true;	// now allow our change handler catch clipboard changes

	// find the window with which the user is working on
	HWND hForeGround = GetForegroundWindow();
	if (hForeGround == NULL)
		return false;

	// release the keys of our hotkey
	INPUT input;
	input.type = INPUT_KEYBOARD;
	input.ki.wScan = 0;
	input.ki.dwFlags = KEYEVENTF_KEYUP;
	input.ki.time = 0;
	input.ki.dwExtraInfo = 0;
	SendInput(1, &input, sizeof(INPUT));
	CRegStdWORD regCaptureTextHK(_T("Software\\MailNoter\\captureTextHK"), DEFAULT_CAPTURE_HOTKEY);
	WORD captureTextHK = (WORD)(DWORD)regCaptureTextHK;
	if (HIBYTE(captureTextHK) & HOTKEYF_ALT)
	{
		input.ki.wVk = VK_MENU;
		SendInput(1, &input, sizeof(INPUT));
	}
	if (HIBYTE(captureTextHK) & HOTKEYF_SHIFT)
	{
		input.ki.wVk = VK_LSHIFT;
		SendInput(1, &input, sizeof(INPUT));
		input.ki.wVk = VK_RSHIFT;
		SendInput(1, &input, sizeof(INPUT));
	}
	if (HIBYTE(captureTextHK) & HOTKEYF_EXT)
	{
		input.ki.wVk = VK_LWIN;
		SendInput(1, &input, sizeof(INPUT));
		input.ki.wVk = VK_RWIN;
		SendInput(1, &input, sizeof(INPUT));
	}
	if (HIBYTE(captureTextHK) & HOTKEYF_CONTROL)
	{
		input.ki.wVk = VK_LCONTROL;
		SendInput(1, &input, sizeof(INPUT));
		input.ki.wVk = VK_RCONTROL;
		SendInput(1, &input, sizeof(INPUT));
	}
	input.ki.wVk = LOBYTE(captureTextHK);
	SendInput(1, &input, sizeof(INPUT));


	// simulate Ctrl-C
	// ctrl down
	input.ki.wVk = VK_LCONTROL;
	input.ki.dwFlags = 0;
	SendInput(1, &input, sizeof(INPUT));

	// C down
	input.ki.wVk = 'C';
	SendInput(1, &input, sizeof(INPUT));

	// C up
	input.ki.dwFlags = KEYEVENTF_KEYUP;
	SendInput(1, &input, sizeof(INPUT));

	// Ctrl up
	input.ki.wVk = VK_LCONTROL;
	SendInput(1, &input, sizeof(INPUT));

	// give the target application time to copy the content to
	// the clipboard. If we don't get a 'clipboard content changed' notification
	// before this timer expires, the timer takes care of cleaning up.
	SetTimer(*this, TIMER_CLIPWAIT, 2000, NULL);

	// now let's hope for a 'clipboard changed' notification, which is where
	// we then do the rest and send the content to the mail client...
	return true;
}